3.6. 使用 GDB 调试多线程程序

由于多个执行流以及并发执行线程之间的交互,调试多线程程序可能会很棘手。一般来说,以下一些事情可以使调试多线程程序变得更容易:

  • 如果可能,请尝试使用尽可能少的线程来调试程序的版本。
  • 在代码中添加调试 printf 语句时,打印出执行线程的 ID 以识别哪个线程正在打印,并以 \n 结束该行。
  • 通过仅让一个线程打印其信息和公共信息来限制调试输出量。例如,如果每个线程将其逻辑 ID 存储在名为 my_tid 的局部变量中,则可以使用 my_tid 值的条件语句将打印调试输出限制为一个线程,如以下示例所示:
if (my_tid == 1) {
    printf("Tid:%d: value of count is %d and my i is %d\n", my_tid, count, i);
    fflush(stdout);
}

3.6.1. GDB 和 Pthread

GDB 调试器特别支持调试线程程序,包括为各个线程设置断点和检查各个线程的堆栈。在GDB中调试Pthreads程序时需要注意的一点是,每个线程至少有三个标识符:

  • Pthreads 库的线程 ID(其 pthread_t 值)。
  • 操作系统的线程的轻量级进程 (LWP) ID 值。该 ID 部分用于操作系统跟踪该线程以进行调度。
  • 线程的 GDB ID。这是在 GDB 命令中指定特定线程时使用的 ID。

线程 ID 之间的具体关系可能因操作系统和 Pthreads 库实现而异,但在大多数系统上,Pthreads ID、LWP ID 和 GDB 线程 ID 之间存在一一对应的关系。

我们介绍了一些用于在 GDB 中调试线程程序的 GDB 基础知识。有关在 GDB 中调试线程程序 的详细信息,请参阅以下内容。

3.6.2. GDB 线程特定命令:

  • 启用打印线程启动和退出事件:
    set print thread-events
  • 列出程序中所有现有的线程(GDB 线程号是列出的第一个值,命中断点的线程用 * 表示):
    info threads
  • 切换到特定线程的执行上下文(例如,在执行 where 时检查其堆栈),通过线程 ID 指定线程:
    thread <threadno>
    
    thread 12        # Switch to thread 12's execution context
    where            # Thread 12's stack trace
  • 仅为特定线程设置断点。在代码中设置断点的地方执行的其他线程不会触发断点来暂停程序并打印GDB提示符:
    break <where> thread <threadno>
    
    break foo thread 12    # Break when thread 12 executes function foo
  • 要将特定的 GDB 命令应用于所有线程或线程子集,请添加前缀thread apply <threadno | all> 为 GDB 命令,其中 threadno 指的是 GDB 线程 ID:
    thread apply <threadno|all> command

这并不适用于每个 GDB 命令,特别是设置断点,因此请使用此语法来设置特定于线程的断点:

    break <where> thread <threadno>

默认情况下,到达断点后,GDB 会暂停所有线程,直到用户输入 cont 。用户可以更改行为以请求 GDB 仅暂停遇到断点的线程,从而允许其他线程继续执行。

3.6.3. 示例:

我们展示了一些 GDB 命令以及在从文件 racecond.c 编译的多线程可执行文件上运行的 GDB 的输出。

这个错误的程序缺乏对共享变量 count 的访问同步。因此,程序的不同运行会产生不同的 count 最终值,这表明存在竞争条件。例如,以下是具有五个线程的程序的两次运行,产生不同的结果:

./a.out 5
hello I'm thread 0 with pthread_id 139673141077760
hello I'm thread 3 with pthread_id 139673115899648
hello I'm thread 4 with pthread_id 139673107506944
hello I'm thread 1 with pthread_id 139673132685056
hello I'm thread 2 with pthread_id 139673124292352
count = 159276966

./a.out 5
hello I'm thread 0 with pthread_id 140580986918656
hello I'm thread 1 with pthread_id 140580978525952
hello I'm thread 3 with pthread_id 140580961740544
hello I'm thread 2 with pthread_id 140580970133248
hello I'm thread 4 with pthread_id 140580953347840
count = 132356636

解决方法是使用 pthread_mutex_t 变量对 count 进行访问。如果用户无法仅通过检查 C 代码来看到此修复,则在 GDB 中运行并在对 count 变量的访问周围放置断点可能会帮助程序员发现问题。

以下是该程序的 GDB 运行中的一些示例命令:

(gdb) break worker_loop   # Set a breakpoint for all spawned threads
(gdb) break 77 thread 4   # Set a breakpoint just for thread 4
(gdb) info threads        # List information about all threads
(gdb) where               # List stack of thread that hit the breakpoint
(gdb) print i             # List values of its local variable i
(gdb) thread 2            # Switch to different thread's (2) context
(gdb) print i             # List thread 2's local variables i

下面的示例显示的是具有 3 个线程的 racecond 程序的 GDB 运行的部分输出(run 3),显示了 GDB 调试会话上下文中的 GDB 线程命令示例。主线程始终是 GDB 线程号 1,三个派生线程是 GDB 线程 2 到 4。

在调试多线程程序时,GDB 用户必须在发出命令时跟踪存在哪些线程。例如,当命中 main 中的断点时,仅存在线程 1(主线程)。因此,GDB 用户必须等到线程创建后才能仅为特定线程设置断点(本示例显示仅在程序中的第 77 行为线程 4 设置断点)。查看此输出时,请注意何时设置和删除断点,并注意当使用 GDB 的 thread 命令切换线程上下文时每个线程的局部变量 i 的值:

$ gcc -g racecond.c -pthread

$ gdb ./a.out
(gdb) break main
Breakpoint 1 at 0x919: file racecond.c, line 28.
(gdb) run 3
Starting program: ...
[Thread debugging using libthread_db enabled] ...

Breakpoint 1, main (argc=2, argv=0x7fffffffe388) at racecond.c:28
28	    if (argc != 2) {
(gdb) list 76
71	  myid = *((int *)arg);
72
73	  printf("hello I'm thread %d with pthread_id %lu\n",
74	      myid, pthread_self());
75
76	  for (i = 0; i < 10000; i++) {
77	      count += i;
78	  }
79
80	  return (void *)0;

(gdb) break 76
Breakpoint 2 at 0x555555554b06: file racecond.c, line 76.
(gdb) cont
Continuing.

[New Thread 0x7ffff77c4700 (LWP 5833)]
hello I'm thread 0 with pthread_id 140737345505024
[New Thread 0x7ffff6fc3700 (LWP 5834)]
hello I'm thread 1 with pthread_id 140737337112320
[New Thread 0x7ffff67c2700 (LWP 5835)]
[Switching to Thread 0x7ffff77c4700 (LWP 5833)]

Thread 2 "a.out" hit Breakpoint 2, worker_loop (arg=0x555555757280)
    at racecond.c:76
76	  for (i = 0; i < 10000; i++) {
(gdb) delete 2

(gdb) break 77 thread 4
Breakpoint 3 at 0x555555554b0f: file racecond.c, line 77.
(gdb) cont
Continuing.

hello I'm thread 2 with pthread_id 140737328719616
[Switching to Thread 0x7ffff67c2700 (LWP 5835)]

Thread 4 "a.out" hit Breakpoint 3, worker_loop (arg=0x555555757288)
    at racecond.c:77
77	      count += i;
(gdb) print i
$2 = 0
(gdb) cont
Continuing.
[Switching to Thread 0x7ffff67c2700 (LWP 5835)]

Thread 4 "a.out" hit Breakpoint 3, worker_loop (arg=0x555555757288)
    at racecond.c:77
77	      count += i;
(gdb) print i
$4 = 1

(gdb) thread 3
[Switching to thread 3 (Thread 0x7ffff6fc3700 (LWP 5834))]
#0  0x0000555555554b12 in worker_loop (arg=0x555555757284) at racecond.c:77
77	      count += i;
(gdb) print i
$5 = 0

(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff77c4700 (LWP 5833))]
#0  worker_loop (arg=0x555555757280) at racecond.c:77
77	      count += i;
(gdb) print i
$6 = 1